package org.mte.sak.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;

import org.mte.sak.util.ClassType;

/**
 * Gestisce l'accesso ad una classe qualsiasi
 * invocando i metodi di questa in maniera riflessiva.
 *
 * @author Marco Traversari
 * @version $Revision: 1.3 $ $Date: 2011-06-16 15:29:30 $
 *
 */
public class ReflectHandler {

	/**
	 * Istanza della classe di riferimento.
	 */
	protected Object object = null;

	/**
	 * Costruttore di default.
	 */
	public ReflectHandler() {
	}

	/**
	 * Costruisce il riferimento alla classe attraverso una
	 * stringa rappresentante il nome completo della classe.
	 *
	 * @param className è il nome completo della classe di riferimento.
	 */
	public ReflectHandler(String className) {
		object = instance(className);
	}

	/**
	 * Costruisce il riferimento alla classe attraverso
	 * un'istanza della classe stessa.
	 *
	 * @param object è l'istanza della classe di riferimento.
	 *
	 */
	public ReflectHandler(Object object) {
		this.object = object;
	}

	/**
	 * Restituisce l'instanza della classe di riferimento.
	 *
	 * @return object è l'istanza della classe.
	 */
	public Object getObject() {
		return object;
	}

	/**
	 * Imposta l'instanza della classe.
	 *
	 * @param object è l'istanza della classe.
	 *
	 */
	public void setObject(Object object) {
		this.object = object;
	}

	/**
	 * Invoca un metodo della classe di riferimento.
	 *
	 * @param <T>
	 *
	 * @param methodName il nome del metodo da invocare.
	 * @param args un array di oggetti di tipo <code>Object</code>
	 * 		rappresentati gli argomenti del metodo invocato.
	 * @param strParameterTypes un array di stringhe rappresentanti
	 * 		il tipo dei parametri del metodo da invocare.
     *
     * @throws Exception
	 *
	 * @return object è l'oggetto prodotto dall'esecuzione del metodo,
	 * 		sarà <code>null</code> se il metodo invocato è di tipo void
	 * 		o non si è potuto invocare il metodo.
	 */
	@SuppressWarnings("unchecked")
	public <T> Object invoke(String methodName, Object[] args,
				String[] strParameterTypes) throws Exception {
		Class<T>[] parameterTypes = buildParameterTypes(strParameterTypes);
		Class<T> cls = (Class<T>) object.getClass();
		Method method = null;
		try {
			method = cls.getMethod(methodName, parameterTypes);
		} catch (NoSuchMethodException nse) {
			method = cls.getDeclaredMethod(methodName, parameterTypes);
		}
        Object returnObject = method.invoke(object, args);
		return returnObject;
	}

	/**
     * Invoca un metodo senza parametri della classe di riferimento.
     *
     * @param methodName è il metodo da invocare.
     * @return un tipo <code>Object</code>
     * @throws Exception
	 */
	public Object invoke(String methodName) throws Exception {
		return invoke(methodName, null, null);
	}

	/**
	 * Invoca un serie di metodi void della classe, i metodi sono descritti
	 * in oggetti di tipo MethodDescriptor.
	 *
	 * @param methodDescriptors
	 * @throws Exception
	 */
	public void invoke(ArrayList<MethodDescriptor> methodDescriptors) throws Exception {
    	if (methodDescriptors != null) {
    		Iterator<MethodDescriptor> methodIterator = methodDescriptors.iterator();
    		while (methodIterator.hasNext()) {
    			MethodDescriptor methodDescriptor = methodIterator.next();
    			Object[] args = null;
    			String[] parameterTypes = null;
    			if (methodDescriptor.getArgDescriptors() != null) {
	    			Iterator<ArgDescriptor> argIterator = methodDescriptor.getArgDescriptors().iterator();
	    			int size = methodDescriptor.getArgDescriptors().size();
	               	args = new Object[size];
	                parameterTypes = new String[size];
	                int i = 0;
	    			while (argIterator.hasNext()) {
	    				ArgDescriptor argDescriptor = argIterator.next();
	                    args[i] = argDescriptor.getValue();
	                    parameterTypes[i] = argDescriptor.getType();
	                    /*System.out.println("argDescriptor=" + argDescriptor
	                         + "-arg[" + i + "]=" + args[i]
	                         + "-parameterTypes[" + i + "]=" + parameterTypes[i]);*/
	                    i++;
	    			}
    			}
    			invoke(methodDescriptor.getName(), args, parameterTypes);
    		}
    	}
	}

	/**
	 * <p>Invoca il metodo <i>set</i> associato al campo
	 * specificato.</p>
     *
     * <p>Il nome del campo deve essere presente nella classe
     * dell'oggetto e non nell'eventuali super classi.</p>
	 *
	 * @param fieldName nome del campo, deve esistere
	 * 			il metodo <code>set-FieldName-(??? value)</code>.
	 * @param value il valore da impostare.
	 * @throws Exception
	 */
	@SuppressWarnings("rawtypes")
	public void set(String fieldName, String value)  throws Exception {
		Field field = object.getClass().getDeclaredField(fieldName);
		Class typeField = field.getType();
		Object objValue = value;
		if (typeField.getName().equals("int")
			|| typeField.getName().equals("java.lang.Integer")) {
			objValue = new Integer(value);
		} else if (typeField.getName().equals("boolean")
			|| typeField.getName().equals("java.lang.Boolean")) {
			objValue = new Boolean(value);
			if (fieldName.indexOf("is") != -1) {
				fieldName = fieldName.substring(2);
			} else if (fieldName.indexOf("has") != -1) {
				fieldName = fieldName.substring(3);
			}
		}
		Object[] args  = new Object[1];
		args[0] = objValue;
		Class[] params = new Class[1];
		params[0] = typeField;
		String methodName = "set" + fieldName.substring(0, 1).toUpperCase() +
							fieldName.substring(1);

		Method method = object.getClass().getMethod(methodName, params);
		method.invoke(object, args);
	}

	/**
	 * <p>Trasforma l'array di stringhe con i nomi completi dei tipi dei
	 * parametri del metodo da invocare in un array di tipo <code>Class</code>.
	 * Nel caso che il tipo parametro sia primitivo ci aspetta che la stringa
	 * contenga solo il nome del tipo primitivo.</p>
	 * <p>Es:</p>
	 * <ul>
	 * <li>strParameterTypes[0] = "int";</li>
	 * <li>strParameterTypes[1] = "boolean";</li>
	 * <ul>
	 *
	 * @param strParameterTypes è una array di tipo <code>String</code>.
	 *
	 * @return restituisce un array di tipo <code>Class</code>.
	 */
	@SuppressWarnings("rawtypes")
	private Class[] buildParameterTypes(String[] strParameterTypes) {
		Class[] parameterTypes = null;
        if (strParameterTypes != null) {
            try {
            	int paramLength = strParameterTypes.length;
				parameterTypes = new Class[paramLength];
				for (int i = 0; i < paramLength; i++) {
					if (strParameterTypes[i].equals(ClassType.BOOLEAN.name)) {
						parameterTypes[i] = boolean.class;
					} else if (strParameterTypes[i].equals(ClassType.CHAR.name)) {
						parameterTypes[i] = char.class;
					} else if (strParameterTypes[i].equals(ClassType.SHORT.name)) {
						parameterTypes[i] = short.class;
					} else if (strParameterTypes[i].equals(ClassType.INT.name)) {
						parameterTypes[i] = int.class;
					} else if (strParameterTypes[i].equals(ClassType.LONG.name)) {
						parameterTypes[i] = long.class;
					} else if (strParameterTypes[i].equals(ClassType.FLOAT.name)) {
						parameterTypes[i] = float.class;
					} else if (strParameterTypes[i].equals(ClassType.DOUBLE.name)) {
						parameterTypes[i] = double.class;
					} else {
						parameterTypes[i] = Class.forName(strParameterTypes[i]);
					}
				}
            } catch (ClassNotFoundException e) {
            	e.printStackTrace();
            }
        }
		return parameterTypes;
	}

	/**
	 * Istanzia un'oggetto dell classe di riferimento attraverso il nome
	 * completo della classe stessa.
	 *
	 * @param className un stringa rappresentante la classe
	 *		da cui istanziare l'oggetto.
	 *
	 * @return l'oggetto istanziato o <code>null</code>
	 * 		se non è stato possibile istanziare l'oggetto.
	 */
	@SuppressWarnings("rawtypes")
	private Object instance(String className) {
		Object object = null;
		try {
			if (className != null) {
				Class objClass = Class.forName(className);
				object = objClass.newInstance();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return object;
	}
}